Skip to content

Release v1.3.0: OTA scheduling, MQTT Inspector, Backup, cleaner option rows#25

Merged
tashda merged 24 commits into
mainfrom
dev
Apr 28, 2026
Merged

Release v1.3.0: OTA scheduling, MQTT Inspector, Backup, cleaner option rows#25
tashda merged 24 commits into
mainfrom
dev

Conversation

@tashda
Copy link
Copy Markdown
Owner

@tashda tashda commented Apr 28, 2026

Summary

Closes the v1.3.0 milestone. Twelve issues:

  • [Feature]: Remove inline descriptions from device state/action cards #13 Cleaner device option rows — drops the verbose Z2M schema description text under each row title in device state/action cards.
  • [Feature]: Add OTA actions to device long-press menu in device list #15 OTA actions in long-press menu — Check for Update (always when OTA-supported), and when an update is available, both Update Now and Schedule Update are exposed regardless of power source. Power source informs ordering: battery devices list Schedule first as the recommended path, mains devices list Update Now first. Cancel Scheduled Update appears when the device is in the scheduled phase.
  • [Feature]: Schedule OTA update for sleepy/battery devices #12 Schedule OTA for sleepy/battery devices — Z2M's schedule and unschedule topics, AppStore optimistic helpers, ViewModel methods, plus integration in three places: (1) device list long-press menu, (2) device detail (...) menu (same battery-aware ordering), (3) 'Check All for Updates' bulk action — mains devices route through the existing rate-limited check queue; battery devices fire schedule requests directly. Device row badge shows 'Scheduled' for the scheduled phase.
  • [Feature]: MQTT topic explorer (Developer Mode) #21 MQTT Inspector (Developer Mode) — toggle in Settings → General. When on, exposes a Developer section with an MQTT Inspector (Subscribe + Publish tabs). Subscribe streams every inbound topic + payload with substring filter, pause, clear, 1000-message ring buffer. Publish sends arbitrary topic + JSON or string payload, with a confirm prompt for bridge/request/* destinations.
  • [Feature]: Backup management — trigger and download Z2M backups #19 Backup management — Settings → Tools → Backup. Triggers bridge/request/backup, decodes the returned base64 zip, writes to a temp file, presents iOS share sheet for save / AirDrop / iCloud Drive. Persists a metadata-only history list. Restore explicitly out of scope.
  • [Bug]: Harden backup pipeline — frame size, base64 strictness, integrity check #30 Harden backup pipeline — three guardrails on the backup save path so users can rely on the file: (1) raise URLSessionWebSocketTask.maximumMessageSize from the 1 MB default to 64 MB so large real-world backups don't exceed the WS frame limit and disconnect, (2) decode base64 with .ignoreUnknownCharacters to tolerate whitespace/newlines that some MQTT serializers insert, (3) post-write integrity check — verify the file is non-empty and starts with the ZIP magic bytes (50 4B 03 04), surface a failed status and delete the partial file otherwise.
  • [Bug]: Connecting without a valid token to a token-required server shows empty homepage instead of failing #27 Fail connection when auth token is missing/invalid — Z2M accepts the WS HTTP 101 upgrade and only then closes with code 1008 when the token is missing/invalid. Previously we marked the session connected on didOpen and navigated to the homepage; the close arrived afterwards and was treated as a normal disconnect. Now Z2MWebSocketSessionDelegate records any close that arrives after the open, and Z2MWebSocketClient waits a 600ms settle window post-handshake to surface the rejection as a connection failure with a clear "Check the auth token." message. The user stays on the connection screen.
  • [Feature]: Tighten settings labels for iOS-style consistency #28 Tighten settings labels for iOS-style consistency — sweep across every bridge-settings page to align with iOS Settings conventions while keeping Z2M vocabulary on the wire. Underlying flags (disable_automatic_update_check, force_disable_retain, disable_led) remain Z2M-canonical; only UI bindings flip.
    • Negated toggles flipped to positive labels with inverted bindings: OTA "Disable Automatic Checks" → "Enable Automatic Checks"; MQTT "Disable Message Retain" → "Retain Messages"; Adapter "Disable Adapter LED" → "Adapter LED" (default ON).
    • Truncated OTA Transfer Timing labels shortened: "Transfer Request Timeout" → "Request Timeout", "Delay Between Blocks" → "Block Delay".
    • Units inlined with the value (never parenthesised in the label): MQTT "Max Packet Size (bytes)" → InlineIntField "Max Packet Size" with unit bytes.
    • Redundant unit words dropped: "Reconnect Attempts" → "Reconnect Limit", "Concurrent Requests" → "Concurrency", "Pause After Retries" → "Pause After".
    • Section / row redundancy collapsed: "Recent Events on Home" → "Recent Events"; both Availability "Offline Timeout" rows → "Timeout"; Health section "Health Check Interval" → "Interval"; Network section "Hardware Tuning" → "Adapter Tuning"; Adapter section "Adapter" → "Connection".
    • Verb-prefix toggles → noun: HA "Use Legacy Action Sensor" → "Legacy Action Sensor", "Use Event Entities" → "Event Entities".
    • Sentence-case fixed: "Automatically share crash reports" → "Automatically Share Crash Reports".
  • [Feature]: Restructure App settings — split Live Activities, slim General, rename Performance #29 Restructure App settings — split Live Activities, slim General, rename Performance — App → General had three unrelated jobs under a misleading "Connection" header with a four-line footer covering all of them.
    • New "Live Activities" subpage (AppLiveActivitiesView) under Application, owns the three LA toggles ("Connection", "OTA Updates", "Scheduled OTAs") split across two sections each with a one-line focused footer.
    • AppGeneralView slimmed to: Appearance / Home / Connection (Reconnect Limit only, one-line footer) / Diagnostics / Advanced (Developer Mode now sits under a proper section header instead of floating).
    • App → Performance renamed to "Bulk OTA" — the page only ever contained that one feature, the previous broad title overpromised. Redundant inner section header removed; Settings root link label updated to match.
  • Inline writable numerics in device settings — match iOS Settings idiom #31 Inline writable numerics in fan settings — under the fan state/action card, writable numeric settings (e.g. countdown_hours, timers) used to render as a row that pushed a near-empty detail screen containing only a slider. They now render inline within the same List row — label + value on top, slider beneath — matching the iOS Settings idiom (Display & Brightness, Accessibility → Display & Text Size). The hero card and indexed-group disclosure rows are untouched; NumericDetailView is removed.
  • [Feature]: Replace custom Behaviour/section card with native List (insetGrouped) on Fan screen #26 Replace custom Behaviour/section card with native List on Fan screen — subsumed by Native iOS Settings sections beneath every category card for leftover exposes #32. Fan's grouped sections (and every other category's) now render as native List Sections with system headers, dividers, dynamic-type, and VoiceOver behaviour, instead of hand-rolled VStack + secondarySystemGroupedBackground cards. The custom sectionView path on FanControlCard is retained only for snapshot contexts (LogDetailView) where there's no surrounding List.
  • Native iOS Settings sections beneath every category card for leftover exposes #32 Native iOS Settings sections beneath every category card for leftover exposes — extends the fan pattern to every device category. Hero cards stay exactly as they are; anything the hero doesn't bind to a primary control drops down as native iOS Settings Sections beneath. linkquality and identify* are always hidden (already surfaced on the device card / noisy diagnostics).
    • New shared primitives: Shared/Components/SettingsFormRow.swift (Expose-driven row with inline slider for writable numerics) and Shared/Components/DeviceExtras.swift (eligibility filter with alwaysHidden list).
    • New per-category section views: LightFeatureSections, SwitchFeatureSections, ClimateFeatureSections, CoverFeatureSections. Fan's existing FanFeatureSections keeps its richer layout (indexed groups + bespoke filter card).
    • Light card: gains rendersAdvancedSheetsInline flag (default true for snapshot contexts). DeviceDetailView passes false, dropping the sunrise (Startup) and ellipsis (More) buttons from inside the card and rendering them as native sections beneath. Effects (sparkles) stays in the card — it's a real light-specific control, not configuration.
    • Switch / Climate / Cover: previously-orphaned exposes (power-on behaviour, child lock, fan mode, preset, calibration, motor speed, etc.) now appear beneath their card grouped via FeatureLayout into proper Behaviour / Indicators / Maintenance / Status / More sections — same taxonomy fan already uses, no flat "Configuration" dump.
    • DeviceDetailView reorganised around a single heroAndSettingsSections(for:state:) builder that dispatches per category — replaces the prior fan-vs-everything-else if/else.
    • Startup ordering: LightControlContext.startupFeatures now sorts so Power-On Behavior leads (Z2M's headline startup setting), followed by state_startupcurrent_level_startupcolor_temp_startup → hue/saturation → execute_if_off. Rows get explicit display labels ("Power-On Behavior", "Startup State") instead of the prettifier's title-cased default.
    • Color-temp parity: the Startup section's "Color Temperature" row now uses the same LightTemperatureControl swatch + tinted slider as the hero card, instead of a plain numeric slider — adjusting startup temperature reads identically to adjusting the live one.
    • Indexed-group sheet (Shared/Components/FeatureGroupDetailView.swift) and section-row dispatcher (DeviceFeatureSectionRow.swift) extracted from fan into shared components so every category renders identical "N members →" disclosures for time1…time5-style indexed families.

Version bumped to 1.3.0.

Closes

Test plan

  • Fast CI passes on PR
  • Long-press a battery device with an update available → Schedule Update appears first, then Update Now
  • Long-press a mains device with an update available → Update Now appears first, then Schedule Update
  • Long-press an OTA-supported device with no update available → only Check for Update appears
  • Schedule a battery device → row shows 'Scheduled' badge; long-press menu shows Cancel Scheduled Update
  • Devices screen → 'Check All for Updates' → mains devices show 'Checking', battery devices show 'Scheduled'
  • Device detail (...) menu mirrors the same Schedule / Update Now / Cancel Scheduled actions
  • Settings → General → toggle Developer Mode → Developer section appears
  • MQTT Inspector Subscribe / Publish tabs work
  • Settings → Tools → Backup → Create Backup completes and produces a valid zip via the share sheet
  • Connect to mock bridge with no token (when AUTH_TOKEN is set on the bridge) → connection fails with "Check the auth token." error, user stays on connection screen
  • Connect to mock bridge with wrong token → same behaviour, error surfaced
  • Connect to mock bridge with correct token → connection succeeds and homepage loads with content (verifies the 600ms settle window doesn't break the happy path)
  • Settings → OTA Updates: toggle reads "Enable Automatic Checks" and is on when bridge has automatic checks enabled (covered by testOTAAutomaticChecksLabelIsPositive)
  • Settings → OTA Updates → Transfer Timing: "Request Timeout", "Block Delay", "Block Size" all visible without truncation (covered by testOTATransferTimingLabelsVisible)
  • Settings → MQTT → Advanced: toggle reads "Retain Messages" (positive); "Max Packet Size" row shows the value with "bytes" trailing (covered by testMQTTRetainLabelIsPositive, testMQTTMaxPacketSizeLabelHasNoParenthesisedUnit)
  • Settings → Adapter: toggle reads "Adapter LED" and is on by default (covered by testAdapterLEDLabelIsPositive)
  • Settings → Home Assistant → Compatibility: toggles read as nouns ("Legacy Action Sensor", "Event Entities") with no "Use" prefix (covered by testHomeAssistantTogglesAreNouns)
  • Settings → General + Performance: numeric labels do not duplicate their unit ("Reconnect Limit", "Concurrency"); covered by testNumericLabelsDoNotRepeatUnit
  • Settings → Availability: timeout rows under each power-source section read "Timeout", not "Offline Timeout" (covered by testAvailabilityTimeoutRowsAreUnqualified)
  • Settings → App → "Live Activities" link present, opens dedicated page with all three toggles (covered by testLiveActivitiesHasOwnPage)
  • Settings → App → General no longer hosts the LA toggles; Reconnect Limit remains (covered by testGeneralNoLongerHostsLiveActivities)
  • Settings → App → "Bulk OTA" replaces "Performance"; navigation pushes a page titled "Bulk OTA" (covered by testBulkOTAReplacesPerformance)
  • Backup hardening — Z2MWebSocketClient raises maximumMessageSize to 64 MB on connect (unit test asserts task config)
  • Backup hardening — saveBackup decodes base64 with embedded newlines/whitespace successfully (unit test feeds a hand-crafted base64 with \n separators)
  • Backup hardening — saveBackup rejects a non-zip payload (wrong magic bytes) and surfaces failed status, partial file is deleted (unit test)
  • Backup hardening — saveBackup rejects empty/zero-byte payload and surfaces failed (unit test)
  • Backup hardening — happy-path manual: drive seeder to emit a >1 MB backup zip, confirm Shellbee receives it without disconnect and the saved file passes unzip -t
  • Open Attic Tuya Fan detail → "Countdown Hours" row in the settings sections renders an inline slider directly beneath the label/value, without tapping; tapping the row does not push a new screen (covered by testFanWritableNumericRendersInline)
  • Open Bathroom Fan detail → hero speed slider still works, settings sections (LED, Child Lock) unchanged
  • NumericDetailView is gone — no dedicated "Value" page is reachable from any fan settings row
  • Open Bedroom Hue → light card shows the Effects (sparkles) button but no longer the sunrise (Startup) or ellipsis (More) buttons; "Startup" and other categorised sections appear as native iOS Settings sections beneath the card (covered by testLightAdvancedFeaturesRenderAsSettingsSections)
  • Open Office Inovelli Fan Switch → previously-hidden switch settings (power-on behaviour, child lock, indicator config) appear beneath the card grouped into Behaviour / Indicators / etc. via FeatureLayout
  • Open a thermostat → fan_mode / preset / calibration / eco features that were previously dropped now surface in their proper FeatureLayout sections
  • Open Bedroom Curtain → calibration / motor settings (when present) surface in the right FeatureLayout section
  • No category surfaces "Linkquality" or "Identify" as a settings row (covered by testFeatureSectionsHideLinkqualityAndIdentify)
  • Open Dining Candle Bulb → Startup section leads with Power-On Behavior (not "Color Temperature" or "Apply Settings While Off"); explicit display labels show "Power-On Behavior" / "Startup State" instead of the prettifier's default
  • Open Dining Candle Bulb → Startup → Color Temperature row uses the same swatch (2.7K / 3K / 4K / 5K / 6.5K) + tinted slider as the hero light card, not a plain numeric slider

🤖 Generated with Claude Code

tashda and others added 5 commits April 28, 2026 14:48
Drops the verbose Z2M schema description text under each option row title
in device state/action cards. Long descriptions (LED intensity / colour
parameters on Inovelli switches, etc.) crowded the list and made it hard
to scan dozens of similar rows.

Fixes #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Z2M's schedule + unschedule OTA topics, AppStore optimistic state
helpers, and ViewModel methods. Context menu now shows:

- Check for Update (always, when device supports OTA)
- Update Now (mains-powered, update available)
- Schedule Update (battery-powered, update available)
- Cancel Scheduled Update (when phase is .scheduled)

The schedule path is the right primitive for sleepy battery devices —
Z2M waits for the device to wake up and request the image rather than
trying to push immediately.

Fixes #15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Behind a Developer Mode toggle in Settings → General, add a Developer
section with an MQTT Inspector. Subscribe tab streams every inbound
topic + payload (substring filter, pause, clear, 1000-message ring).
Publish tab sends arbitrary topic + JSON or string payload, with a
confirm prompt for bridge/request/* destinations.

Z2MMessageRouter exposes decodeRaw() so the session controller can tap
inbound messages for the inspector without breaking the typed routing.

Fixes #21

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings → Tools → Backup. Sends bridge/request/backup, decodes the
returned base64 zip from bridge/response/backup, writes it to a temp
file, and presents iOS share sheet for save / AirDrop / iCloud Drive.
Persists a metadata-only history list (timestamp + size, last 20
backups) — Shellbee does not retain backup files.

Restore is intentionally out of scope: Z2M does not expose a restore
API, restoration requires host access to the data directory. Links to
the Z2M restore guide.

Fixes #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#15 revision: Both 'Update Now' and 'Schedule Update' are now exposed
on every OTA-supported device (not just battery). Power source informs
ordering — battery devices list Schedule first as the recommended path,
mains devices list Update Now first — but the user can pick either on
any device.

#12 implementation:
- Device detail (...) menu: same Schedule / Update Now / Cancel
  Scheduled actions, ordered by power source.
- 'Check All for Updates' bulk action: mains devices route to the
  rate-limited check queue; battery devices fire schedule requests
  directly (no rate-limit needed — Z2M defers them until each device
  wakes up).
- Device row badge: shows 'Scheduled' instead of generic 'Preparing'
  when phase is .scheduled.

Fixes #12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tashda tashda added the area:ota OTA / firmware updates label Apr 28, 2026
tashda and others added 2 commits April 28, 2026 15:06
…hare preview

The 'Restore guide' link pointed at an outdated zigbee2mqtt.io URL that
404s. Replaced with an in-app sheet (RestoreGuideSheet) that explains
why Shellbee can't restore (no MQTT API for it, requires host access)
and walks through the steps to do it from the Z2M host. No external
links for the core content.

Backup files now write to Documents/Backups/ instead of temporaryDirectory.
The temp directory's sandboxed URLs caused the iOS share sheet to fall
back to a minimal set of receivers ("Save to Files" only — no AirDrop,
Mail, Messages, third-party apps). Documents/Backups/ is durable and
exposes the full receiver list. ShareLink also gets an explicit
SharePreview so the receiver shows a proper file label and zip glyph.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… CTA

Three fixes:

- State loss on tab switch. Subscribe/Publish were sibling subviews
  inside a switch — toggling destroyed the active view and lost the
  message buffer. Lifted into an @observable SubscribeStore owned by
  MQTTInspectorView so the buffer persists for the inspector lifetime.

- Native chrome. Segmented control moved into the navigation toolbar
  (principal placement). Filter is now a .searchable bar. Pause / Clear
  moved to topBarTrailing icon buttons. List uses .plain style with a
  ContentUnavailableView for the empty state. Each message row gets a
  payload card with a tertiary fill background and a Show more / less
  toggle when content exceeds six lines.

- Publish button. Was a plain Form row; now a borderedProminent
  large-control CTA pinned below the form on a .bar background, full
  width with a paperplane glyph. Topic field has submitLabel(.next) and
  hands focus to the payload editor on return.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tashda tashda added this to the v1.3.0 milestone Apr 28, 2026
@tashda tashda added the area:dev-tools Developer / power-user tooling label Apr 28, 2026
@tashda tashda self-assigned this Apr 28, 2026
tashda and others added 2 commits April 28, 2026 15:27
Three fixes for the inspector chrome:

- JSON syntax highlighting on subscribe payloads. Keys blue, string
  values green, numbers orange, booleans/null purple, structure muted
  secondary. Topic row also picks up the bridge/logging level color +
  glyph (red/yellow/blue/gray) when the message is a log entry, matching
  the raw logs view treatment.

- Stable picker position. Switched from .frame(maxWidth:) to a fixed
  220pt width on the segmented control, and consolidated trailing
  toolbar items to one per tab. Subscribe gets a single Menu (Pause +
  Clear inside); Publish gets a single Reset Form button. Picker stays
  in the same spot regardless of tab.

- Unified Publish layout. Dropped the separate .bar bottom strip — the
  Publish button is now a borderedProminent CTA inside the Form's last
  Section with a clear row background, so the page reads as one
  continuous form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OTA: "Disable Automatic Checks" → "Enable Automatic Checks" (positive
  toggle, inverted binding; underlying disable_automatic_update_check
  flag still written negated).
- OTA Transfer Timing: shorten labels so they no longer truncate
  ("Transfer Request Timeout" → "Request Timeout", "Delay Between
  Blocks" → "Block Delay").
- MQTT: "Disable Message Retain" → "Retain Messages" (same negated-flag
  pattern, inverted binding); footer copy updated.
- MQTT: "Max Packet Size (bytes)" now uses InlineIntField so the unit
  renders alongside the value, matching every other numeric row.
- Add UI tests for each label change.

Fixes #28
tashda and others added 13 commits April 28, 2026 20:43
Drops verb prefixes, removes redundant unit/qualifier words, fixes
sentence-case stragglers, flips one more negated toggle:

- AppGeneralView: "Recent Events on Home" → "Recent Events"
  (section header already says Home); "Reconnect Attempts" →
  "Reconnect Limit" (unit "attempts" was repeated);
  "Automatically share crash reports" → Title Case.
- AppPerformanceView: "Concurrent Requests" → "Concurrency" (same
  unit-repeat).
- AvailabilitySettingsView: both "Offline Timeout" rows → "Timeout"
  (sections disambiguate); "Pause After Retries" → "Pause After".
- HealthSettingsView: section "Health Check Interval" → "Interval"
  (row inside is "Check Interval").
- HomeAssistantSettingsView: drop "Use" prefix on toggles
  ("Use Legacy Action Sensor" → "Legacy Action Sensor", same for
  Event Entities).
- NetworkSettingsView: section "Hardware Tuning" → "Adapter Tuning".
- SerialSettingsView: "Disable Adapter LED" → "Adapter LED" with
  inverted binding (default ON, user disables); section "Adapter"
  (containing Adapter Type / Baud Rate / RTS-CTS) → "Connection".

Adds UI tests for each rename.

Fixes #28
…e Performance

App → General was doing three unrelated jobs under a misleading
"Connection" header (Live Activity toggles + scheduled-OTA opt-in +
reconnect retry limit) with a four-line footer trying to cover them
all. Splits it apart into focused pages.

- New AppLiveActivitiesView (linked from Application section between
  General and Notifications): three toggles "Connection",
  "OTA Updates", "Scheduled OTAs" across two sections, each with a
  one-line footer instead of the previous wall of text.
- AppGeneralView slimmed: Appearance / Home / Connection
  (Reconnect Limit only) / Diagnostics / Advanced (Developer Mode now
  has a section header instead of floating).
- AppPerformanceView renamed to "Bulk OTA" — the page only ever
  contained that one feature; the broad "Performance" title
  overpromised. Removed the redundant section header. Settings root
  link label updated to match.
- Added UI tests covering the new navigation and the absence of the
  legacy labels.

Fixes #29
… of OTA Updates

Two sections made the three toggles look like two unrelated features.
Collapses to one section with a combined footer; the disabled state on
Scheduled OTAs (when OTA Updates is off) already communicates the
parent/child relationship.

Refs #29
Reorders the About page so the section the user actually came for —
which version am I running, where are the device stats / credits — is
the first thing they see. Bridge / Zigbee Network details follow.

- New top "Shellbee" section: Version, Build, Device Statistics link,
  Acknowledgements link. Drops the previous unnamed footer-section
  that hosted the two nav links.
- Acknowledgements: add Sentry Cocoa SDK (MIT). It's the only
  third-party SDK shipped with the app (per PRIVACY.md), and it
  belongs alongside the Z2M / zigbee2mqtt.io entries.

Refs #29
About → new "Connect" section between Shellbee and Bridge, with two
iOS-native rows: "Rate Shellbee" (star, pink) and "View on GitHub"
(code chevron, label colour). Each uses the Settings-style coloured
icon tile + trailing arrow.up.right indicator. App Store URL is a
TBD placeholder until the App ID is assigned at first TestFlight.

Footer copy fixes that became stale after recent label flips:

- SerialSettingsView: footer was "Turns off the indicator LED…"
  but the toggle is now "Adapter LED" (default ON, user disables).
  Rewritten as "Controls the indicator LED on the Zigbee adapter,
  if supported."
- AvailabilitySettingsView: said "Shellbee tracks…" but the bridge
  does the tracking; corrected to "the bridge tracks…".
- OTASettingsView Transfer Timing: referenced the old "Delay" label;
  updated to match the renamed "Block Delay".

Refs #29
Commit 478cb6b added AppLiveActivitiesView referencing
ConnectionSessionController.otaScheduledLiveActivityEnabledKey but
never landed the matching `static let` declaration, breaking Fast CI
since. Adds the constant. Also restores a paste-corrupted
extension declaration in InterviewActivityWidget that the same broken
state would have masked behind the earlier compile failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related Z2M WebSocket robustness changes.

Early auth rejection — Z2M completes the WS handshake first, then
either streams the cached bridge state or closes the socket with
1008/policy-violation when the auth token is wrong. Without waiting
for the first inbound frame we'd report "connected" and only fail on
the next send. Now waits up to 5s for the first message, surfaces a
clear "Server rejected the connection. Check the auth token." on
1008 closes, and replays the validated message into the stream so
the session controller still sees it.

Frame size — `bridge/response/backup` carries the full Z2M data
folder as a base64 blob inside one JSON frame. The default 1 MB
URLSessionWebSocketTask cap aborts on populated installs. Raises to
64 MB, enough headroom for typical mesh configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the base64-decode + zip integrity check out of BackupView into
a nonisolated BackupPayload enum so it's unit-testable without
SwiftUI. Verifies the decoded file starts with the PK\\x03\\x04 zip
magic before declaring success — base64 decoding is lenient enough
that an HTML error page or truncated payload would silently produce a
non-zip "backup". A failed verification deletes the bogus file and
surfaces the failure to the user.

UI: consolidates Create + Share into one section, shows the backup
size next to "Share Backup" instead of in a status row, and uses a
LabeledContent layout for history entries. Restore Guide row gets a
chevron affordance and the share sheet routes through
UIActivityViewController so the user can save to Files / iCloud /
AirDrop without ShareLink's preview quirks on iPad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Z2M's "scheduled" OTA phase (parked, waiting for the device to wake)
was effectively invisible across the app — devices fell out of the
Updates filter the moment they were scheduled, the badge ring went
indeterminate, and Home alerts didn't differentiate scheduled vs.
in-progress.

- DeviceCondition.updatesAvailable now also matches scheduled /
  requested / updating phases via an optional otaStatus argument, so
  scheduled devices stay in the filter.
- DeviceListRow swipe actions: a scheduled row exposes "Cancel"
  (calls unschedule). Battery devices get Schedule before Update;
  mains devices keep Update before Schedule.
- DeviceUpgradeBadgeView renders a static ring + clock.badge glyph
  for scheduled — no spinner, since the device is parked.
- DeviceListViewModel + DeviceDetailView re-issue an OTA check after
  unschedule. Z2M leaves update.state at "idle" otherwise, which
  drops the device out of the Updates filter entirely.
- DeviceFirmwareMenu: drops the battery/mains split for the bulk
  "Check All" button. Z2M only offers a synchronous OTA check;
  routing battery devices through schedule was a workaround. Now
  every device goes through the rate-limited bulk queue, and sleepy
  devices that don't respond surface the standard error like
  windfront. Empty-state label flips to "No Updates" with a checkmark
  glyph.
- HomeSnapshot adds scheduledUpdateDevices + updatingDevices counts;
  HomeDevicesCard renders dedicated alert rows for each.
- OTAUpdateLiveActivityCoordinator gains an isScheduledEnabled flag
  (UserDefault, off by default) — scheduled OTAs can sit pending for
  hours, and most users don't want a Lock Screen surface for that.
  AppLiveActivitiesView's "Scheduled OTAs" toggle drives it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switching from a working server to one with bad/missing auth left
hasBeenConnected == true from the prior session, which routed the
new failure into the .lost branch — leaving the user on a stale
homepage instead of bouncing back to the setup screen.

connect() now drops hasBeenConnected, resets the store, and clears
isConnected before kicking off the session, so a failure cleanly
surfaces as .failed on a fresh attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fan extras section was rendering each Expose with a custom row
(leading icon, custom paddings) and presenting indexed groups via a
.sheet. Aligns with iOS Settings conventions instead.

- New SettingsFormRow renders one Expose as a plain Form row: label
  on the left, value or control on the right, no leading icon, no
  chevron. Writable numerics push a detail screen with a slider.
- Indexed groups now drill in through a NavigationLink to a
  FeatureGroupDetailView instead of bouncing through a sheet —
  matches the rest of the device detail navigation and keeps the
  back-stack coherent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the Rate / GitHub group a labelled section instead of an
unlabelled one, and applies .buttonStyle(.plain) so the rows
don't pick up the default tinted button styling. The GitHub row
icon shifts to .darkGray so it reads consistently in dark mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tashda and others added 2 commits April 28, 2026 22:44
Wrap the row HStack in `.contentShape(Rectangle())` so taps on the
empty space between the label and the chevron register, matching the
hit-test behaviour of native iOS Settings rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hero cards stay exactly as they are; everything they don't bind to a
primary control drops down as native iOS Settings `Section`s beneath,
grouped via `FeatureLayout` into Behaviour / Indicators / Maintenance /
Status / More sections — same taxonomy fans already use.

Writable numerics render their slider inline within the same `List`
row (label + value on top, slider beneath) instead of pushing a
near-empty detail screen with just a slider. `NumericDetailView` is
gone.

`linkquality`, `battery`, `last_seen`, `update`, `update_available`,
and any `identify*` prefix are always hidden — surfaced on the device
card or noisy diagnostics, never in a settings list.

Light: removes the sunrise (Startup) and ellipsis (More) sheet buttons
from inside the card and renders them as native sections beneath.
Effects (sparkles) stays in the card — true light-specific control.
Startup section sorts Power-On Behavior first, then state / brightness
/ color-temp / hue+sat / execute_if_off. The Color Temperature row
now uses the same swatch + tinted slider as the hero card.

`DeviceDetailView` reorganised around a single
`heroAndSettingsSections(for:state:)` builder.

Shared primitives: `SettingsFormRow`, `DeviceExtras`,
`DeviceFeatureSectionRow`, `FeatureGroupDetailView` extracted into
`Shared/Components/` so every category dispatches identically.

Fixes #31
Fixes #32

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tashda tashda added area:ui UI / UX redesign area:onboarding Onboarding / pairing / first-launch area:diagnostics Diagnostics / mesh health priority:high enhancement New feature or request bug Something isn't working labels Apr 28, 2026
@tashda tashda merged commit 8990296 into main Apr 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment